import * as THREE from 'three';
import TWEEN from '@tweenjs/tween.js';
import Dispatcher from './dispatcher';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader';
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { throttle } from './utils';

/**
 * 模型加载类
 */
class HtMatrix extends Dispatcher {
  constructor(selector, options) {
    super();
    this.options = options;
    this.tracker = new Set();
    this.container = document.getElementById(selector);
    this.height = this.container.clientHeight;
    this.width = this.container.clientWidth;
    this.range = this.container.getBoundingClientRect();
    this.labelPanels = new THREE.Group();
    this.animationID = 0;
    this.initial();
  }

  // 初始化
  initial() {
    this.createArena();
    this.createObject();
    this.handleClick();
    this.handleResize();
    this.handleMousemove();
    this.createAnimation();
  }

  createObject() {
    const { options } = this;
    if (!options.url) return;
    this.createObject3D(options.url).then(res => {
      this.object = res.scene;
      options.scale && this.object.scale.set(...options.scale); // 缩放模型
      options.rotation && this.object.rotation.set(...options.rotation); // 模型角度
      options.position && this.object.position.set(...options.position); // 模型位置
      this.scene.add(this.object);
      this.dispatchEvent('renderer', this);
    });
  }

  // 创建舞台
  createArena() {
    const { width, height, options, container } = this; // 获取尺寸
    const scene = new THREE.Scene(); // 创建场景
    const camera = new THREE.PerspectiveCamera(30, width / height, 1, 5000); // 创建相机
    camera.position.x = options.camera.x;
    camera.position.y = options.camera.y;
    camera.position.z = options.camera.z;

    // 创建灯光
    const ambientLight = new THREE.AmbientLight(0xefefef, 1); // 添加环境光
    const directionalLight = new THREE.DirectionalLight(0xfffffff, 0.8); // 创建平行光
    directionalLight.position.set(300, 200, 400);
    scene.add(ambientLight);
    scene.add(directionalLight);

    // 模型渲染
    const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true,
      logarithmicDepthBuffer: true,
      premultipliedAlpha: false,
      depthWrite: false,
      preserveDrawingBuffer: true,
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.outputEncoding = THREE.GammaEncoding;
    renderer.setClearColor(0xe3dfde, 0);
    renderer.sortObjects = false;
    renderer.setSize(width, height);
    container.appendChild(renderer.domElement);

    // 创建渲染器容器
    const composer = new EffectComposer(renderer);
    const renderPass = new RenderPass(scene, camera);
    composer.addPass(renderPass);

    // 创建阴影效果
    const effectFXAA = new ShaderPass(FXAAShader);
    effectFXAA.uniforms['resolution'].value.set(1 / width, 1 / height);
    composer.addPass(effectFXAA);

    // 创建高亮边框
    const outlineParams = {
      edgeStrength: 3.0,
      edgeGlow: 0.0,
      edgeThickness: 1,
      pulsePeriod: 0,
      rotate: false,
      usePatternTexture: {
        wrapS: THREE.RepeatWrapping,
        wrapT: THREE.RepeatWrapping,
      },
    };
    const outlinePass = new OutlinePass(new THREE.Vector2(width, height), scene, camera);
    outlinePass.edgeStrength = Number(outlineParams.edgeStrength);
    outlinePass.edgeGlow = Number(outlineParams.edgeGlow);
    outlinePass.edgeThickness = Number(outlineParams.edgeThickness);
    outlinePass.pulsePeriod = Number(outlineParams.pulsePeriod);
    outlinePass.visibleEdgeColor.set('#fd0000');
    outlinePass.hiddenEdgeColor.set('#fd0000');
    composer.addPass(outlinePass);

    // 创建覆盖层（弹窗）
    const labelRenderer = new CSS2DRenderer(); // 新建CSS2DRenderer
    labelRenderer.setSize(width, height);
    labelRenderer.domElement.style.position = 'absolute';
    labelRenderer.domElement.style.top = 0;
    container.appendChild(labelRenderer.domElement);

    // 创建控制器
    const controls = new OrbitControls(camera, labelRenderer.domElement);
    controls.enablePan = true;
    controls.enableDamping = true;
    controls.rotateSpeed = 0.4;
    controls.maxPolarAngle = Math.PI * 0.49;
    controls.maxDistance = 1000;

    // 解压模型
    const dracoLoader = new DRACOLoader(); // 解压模型
    dracoLoader.setDecoderPath(options.draco); // 设置解压库文件路径

    this.scene = scene;
    this.camera = camera;
    this.composer = composer;
    this.renderer = renderer;
    this.controls = controls;
    this.effectFXAA = effectFXAA;
    this.outlinePass = outlinePass;
    this.dracoLoader = dracoLoader;
    this.labelRenderer = labelRenderer;
  }

  each(callback) {
    const { object } = this;
    object.traverse(child => callback(child));
  }

  /**
   * 加载模型
   * @param {*} url 模型URL
   */
  createObject3D(url) {
    return new Promise((resolve, reject) => {
      const { dracoLoader } = this;
      const loader = new GLTFLoader(); // 加载模型
      loader.setCrossOrigin('anonymous');
      loader.setDRACOLoader(dracoLoader); // 设置gltfloader解压loader
      loader.load(url, resolve, progress => this.dispatchEvent('progress', progress), reject);
    });
  }

  // 添加自定义数据
  addUserData(vals) {
    const { object } = this;
    object.traverse(child => {
      const name = child.name;
      const data = child.userData;
      const findData = vals.find(item => new RegExp(item.patt).test(name)) || {};
      child.userData = { ...data, data: findData.data };
    });
  }

  // 转场动画
  createAnimation() {
    const { scene, camera, controls, composer, labelRenderer } = this;
    const rotationAnimation = () => {
      controls.update();
      composer.render(); // 用效果合成器渲染
      labelRenderer.render(scene, camera);
      this.animationID = requestAnimationFrame(rotationAnimation);
      TWEEN.update();
    };

    rotationAnimation();
  }

  // 加载模型列表
  createObject3DList() {
    const { options } = this;
    if (!options.children || options.children.length < 1) return;
    return Promise.all(options.children.map(item => this.createObject3D(item.url)));
  }

  /**
   * 获取模型
   * @param {Object} event 事件返回值
   */
  getObjectBySite(event) {
    let intersects = null;
    const { width, range, height, camera, object } = this;
    const mouse = new THREE.Vector2();
    const raycaster = new THREE.Raycaster();
    const offsetTop = event.clientY - range.top;
    const offsetLeft = event.clientX - range.left;

    if (!object) return;
    mouse.x = (offsetLeft / width) * 2 - 1;
    mouse.y = -(offsetTop / height) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    intersects = raycaster.intersectObjects(object.children, true);
    if (!intersects[0]) return false;
    if (!intersects[0].object) return false;
    return intersects[0].object;
  }

  /**
   * 获取模型对象
   * @param {*} name 模型名字
   */
  getObjectByName(name) {
    const { object } = this;
    return object.getObjectByName(name);
  }

  /**
   * 添加高亮边框
   * @param {Object} model 模型对象
   */
  addOutlinePass(model) {
    const selectedObjects = [];
    const { outlinePass } = this;
    model && selectedObjects.push(model);
    outlinePass.selectedObjects = selectedObjects;
  }

  /**
   * 删除模型
   * @param {Object} model 模型对象
   */
  removeObject(model) {
    this.object.remove(model);
  }

  /**
   * 移除模型对象
   * @param {*} name 模型名字
   */
  removeObjectByName(name) {
    const { object } = this;
    object.remove(object.getObjectByName(name));
  }

  /**
   * 根据模型名字切换场景
   * @param {String} name 模型名字
   * @param {Object} point 位置增量
   */
  switchSceneByName(name, point) {
    const { scene, object, camera, controls, labelPanels } = this;
    scene.remove(labelPanels);
    if (!object.getObjectByName(name)) return; // 模型不存在直接返回
    const matrix = object.getObjectByName(name); // 获取模型对象
    const locate = matrix.getWorldPosition(); // 获取模型坐标
    const locateX = locate.x + (point ? point.x : 0);
    const locateY = locate.y + (point ? point.y : 24);
    const locateZ = locate.z + (point ? point.z : 24);
    new TWEEN.Tween(camera.position).to({ x: locateX, y: locateY, z: locateZ }, 500).start();
    new TWEEN.Tween(controls.target).to({ x: locate.x, y: locate.y, z: locate.z }, 500).start();
    matrix.visible = true;
  }

  /**
   * 根据模型对象切换场景
   * @param {Object} name 模型对象
   * @param {Object} name 位置增量
   */
  switchSceneByObject(model, point) {
    const { scene, camera, controls, labelPanels } = this;
    scene.remove(labelPanels);
    const locate = model.getWorldPosition(); // 获取模型坐标
    const locateX = locate.x + (point ? point.x : 0);
    const locateY = locate.y + (point ? point.y : 24);
    const locateZ = locate.z + (point ? point.z : 24);
    new TWEEN.Tween(camera.position).to({ x: locateX, y: locateY, z: locateZ }, 500).start();
    new TWEEN.Tween(controls.target).to({ x: locate.x, y: locate.y, z: locate.z }, 500).start();
    model.visible = true; // 显示模型
  }

  /**
   * 显示所有模型
   * @param {*} assert 回调函数，必须返回值
   */
  visibleAllObject(assert) {
    const { scene, object, labelPanels } = this;
    object.traverse(child => {
      if (assert(child)) {
        child.material.opacity = 1;
        child.material.transparent = false;
      }
    });
    scene.remove(labelPanels);
    scene.updateMatrixWorld(true);
  }

  /**
   * 显示选中模型
   * @param {*} model 模型对象
   */
  visibleActiveObject(model) {
    const { object } = this;
    object.traverse(child => {
      if (child.type !== 'Mesh') return;
      const isEqual = child === model;
      child.material.opacity = isEqual ? 1 : 0.5;
      child.material.transparent = !isEqual;
    });
  }

  /**
   * 显示选中模型
   * @param {*} model 模型名字
   */
  visibleActiveObjectByName(name) {
    const { object } = this;
    const matrix = object.getObjectByName(name);
    if (!matrix) return; // 没有找到模型就返回
    object.traverse(child => {
      if (child.type !== 'Mesh') return;
      const isEqual = child === matrix;
      child.material.opacity = isEqual ? 1 : 0.3;
      child.material.transparent = !isEqual;
    });
  }

  /**
   * 添加高亮边框
   * @param {*} name 模型高亮
   */
  addOutlinePassByName(name) {
    const selectedObjects = [];
    const { object, outlinePass } = this;
    const matrix = object.getObjectByName(name);
    selectedObjects.push(matrix);
    outlinePass.selectedObjects = selectedObjects;
  }

  // 获取标签文字
  obtainLabelsTexts(data, value, prop) {
    const result = [];
    for (const val of data) {
      const element = value.list.find(elem => elem[prop] === val);
      if (element) result.push(`${val}：${element.value}`);
    }
    return result;
  }

  // 绘制标签气泡
  createLabelsBubble(data, opts) {
    const { font, pane, scale } = opts;
    const length = data.length;
    const radius = pane.radius * scale;
    const padding = pane.padding * scale;
    const triangle = pane.triangle * scale;
    const fontSize = font.size * scale;
    const lineHeight = font.lineHeight * scale;
    const borderWidth = pane.borderWidth * scale;
    const fontStyle = `${font.weight || 'normal'} ${fontSize || 16}px ${font.family || 'Arial'}`;

    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');

    context.font = fontStyle;

    const texts = data.map(text => context.measureText(text).width);
    const width = Math.ceil(Math.max(...texts) + padding * 2); // 气泡的宽度
    const height = lineHeight * length + padding * 2 + triangle; // 气泡的高度

    // 绘制背景
    const top = borderWidth; // 上线
    const left = borderWidth; // 左线
    const right = width - borderWidth; // 右线
    const bottom = height - borderWidth - triangle; // 下线
    const radiusL = left + radius; // 左上圆心
    const radiusT = top + radius; // 右上圆心
    const radiusR = right - radius; // 右下圆心
    const radiusB = bottom - radius; // 左下圆心

    canvas.width = width;
    canvas.height = height;
    context.beginPath();
    context.moveTo(left, radiusT);
    context.arc(radiusL, radiusT, radius, Math.PI, Math.PI * 1.5);
    context.lineTo(radiusL, top);
    context.arc(radiusR, radiusT, radius, Math.PI * 1.5, Math.PI * 2);
    context.lineTo(right, radiusT);
    context.arc(radiusR, radiusB, radius, Math.PI * 2, Math.PI / 2);
    context.lineTo(width / 2 + triangle + borderWidth, bottom);
    context.lineTo(width / 2, height - borderWidth);
    context.lineTo(width / 2 - triangle - borderWidth, bottom);
    context.lineTo(radiusL, bottom);
    context.arc(radiusL, radiusB, radius, Math.PI / 2, Math.PI);
    context.lineTo(left, radiusT);
    context.closePath();
    context.lineWidth = borderWidth * 2;
    context.fillStyle = pane.backgroundColor || '#ff0000';
    context.strokeStyle = pane.borderColor || '000000';
    context.stroke();
    context.fill();

    // 绘制文字
    data.forEach((text, index) => {
      const yAxis = padding + lineHeight / 2 + lineHeight * index;
      context.font = fontStyle;
      context.textBaseline = 'middle';
      context.fillStyle = font.color || '#ffffff';
      context.fillText(text, padding, yAxis);
    });

    return canvas;
  }

  // 创建精灵标签
  createLabelsSprite(data, model, opts) {
    const { scene } = this;
    const { position, userData } = model;
    if (!userData || !userData.data) return;

    scene.remove(this.labelPanels);
    this.labelPanels = new THREE.Group();
    userData.data.forEach((item, index) => {
      const items = this.obtainLabelsTexts(item.data, data, opts.prop);
      const canvas = this.createLabelsBubble(items, opts);
      const texture = new THREE.Texture(canvas);
      const material = new THREE.SpriteMaterial({ map: texture, color: 0xffffff });
      const sprite = new THREE.Sprite(material);
      const spriteMaterial = sprite.material;
      const imageWidth = spriteMaterial.map.image.width / opts.scale;
      const imageHeight = spriteMaterial.map.image.height / opts.scale;
      const scaleWidth = (imageWidth / 100) * (opts.showScale + 1);
      const scaleHeight = (imageHeight / 100) * (opts.showScale + 1);

      texture.needsUpdate = true;
      texture.matrixAutoUpdate = false;
      sprite.position.x = position.x + item.position.x;
      sprite.position.y = position.y + item.position.y + imageHeight / 50;
      sprite.position.z = position.z + item.position.z;
      sprite.scale.set(scaleWidth, scaleHeight, 1.0);
      sprite.material.depthWrite = false;
      this.labelPanels.add(sprite);
    });
    scene.updateMatrixWorld(true);
    scene.add(this.labelPanels);
  }

  // 窗口改变
  resizeHandler() {
    const { camera, renderer, composer, effectFXAA, container, labelRenderer } = this;
    this.height = container.offsetHeight;
    this.width = container.offsetWidth;
    this.range = container.getBoundingClientRect();
    camera.aspect = this.width / this.height;
    camera.updateProjectionMatrix();
    renderer.setSize(this.width, this.height);
    composer.setSize(this.width, this.height);
    labelRenderer.setSize(this.width, this.height);
    effectFXAA.uniforms['resolution'].value.set(1 / this.width, 1 / this.height);
  }

  // 移动事件
  handleMousemove() {
    const { labelRenderer } = this;
    const EventHandler = throttle(event => this.dispatchEvent('mousemove', this.getObjectBySite(event) || {}), 20);
    labelRenderer.domElement.addEventListener('mousemove', EventHandler, false);
  }

  // 窗口改变
  handleResize() {
    const EventHandler = throttle(event => {
      this.resizeHandler();
      this.dispatchEvent('resize', event);
    }, 100);

    window.addEventListener('resize', EventHandler, false);
  }

  // 双击事件
  handleClick() {
    const { labelRenderer } = this;
    const EventHandler = event => this.dispatchEvent('click', this.getObjectBySite(event) || {});
    labelRenderer.domElement.addEventListener('dblclick', EventHandler, false);
  }

  /**
   * 添加模型
   * @param {Object} model 模型对象
   */
  addObject(model) {
    this.object.add(model);
  }

  // 恢复原样
  resetScene() {
    const { camera, options, controls, outlinePass } = this;
    const cameraPosition = { x: options.camera.x, y: options.camera.y, z: options.camera.z };
    outlinePass.selectedObjects = [];
    new TWEEN.Tween(camera.position).to(cameraPosition, 500).start();
    new TWEEN.Tween(controls.target).to({ x: 0, y: 0, z: 0 }, 500).start();
  }

  track(obj) {
    if (!obj) return obj;
    if (Array.isArray(obj)) {
      obj.forEach(item => this.track(item));
      return obj;
    }

    if (obj.dispose || obj instanceof THREE.Object3D) {
      this.tracker.add(obj);
    }

    if (obj instanceof THREE.Object3D) {
      this.track(obj.geometry);
      this.track(obj.material);
      this.track(obj.children);
    } else if (obj instanceof THREE.Material) {
      for (const value of Object.values(obj)) {
        if (value instanceof THREE.Texture) this.track(value);
      }

      if (obj.uniforms) {
        for (const value of Object.values(obj.uniforms)) {
          if (value) {
            const uniformValue = value.value;
            if (uniformValue instanceof THREE.Texture || Array.isArray(uniformValue)) {
              this.track(uniformValue);
            }
          }
        }
      }
    }
    return obj;
  }

  // 清除模型
  clear() {
    const { object, animationID } = this;
    const gl = this.renderer.domElement.getContext('webgl');
    gl && gl.getExtension('WEBGL_lose_context').loseContext();
    cancelAnimationFrame(animationID);

    this.track(object);
    this.scene.clear();
    this.scene.remove();
    this.renderer.clear();
    this.renderer.dispose();
    this.renderer.forceContextLoss();
    this.renderer.content = null;
    this.renderer.domElement = null;
    this.renderer.info.reset();
    this.tracker.forEach(item => {
      if (item.dispose) item.dispose();
      if (item instanceof THREE.Object3D) object.remove(item);
    });

    this.scene = null;
    this.camera = null;
    this.composer = null;
    this.controls = null;
    this.effectFXAA = null;
    this.outlinePass = null;
    this.dracoLoader = null;
    this.labelRenderer = null;
    this.tracker.clear();
    this.object.remove();
    this.object = null;
    this.renderer = null;
  }
}

export default HtMatrix;
